/// File: VSLabFSICore\package.fs
/// 
/// Author: Antonio Cisternino (cisterni@di.unipi.it)
///   
/// ------------------------------------------------------------
/// Copyright (c) Microsoft Corporation.  All rights reserved.
/// 
/// The use and distribution terms for this software are 
/// contained in the file named license.txt, which can be found 
/// in the root of this distribution.
/// By using this software in any fashion, you are agreeing to 
/// be bound by the terms of this license.
///
/// You must not remove this notice, or any other, from this
/// software.
/// ------------------------------------------------------------

module VSLab.Packages

open System
open System.IO
open System.Reflection
open System.Xml
open VSLab.Environment

let LoadedPackages = new System.Collections.Generic.Dictionary<string, bool>()

/// <summary>
/// Attribute used to decorate packages. It is an assembly level attribute.
/// </summary>
/// <param name="t">The type of the package manager, it must inherit from VSLab.Packages.Pacakge class.</param>
[<AttributeUsage(AttributeTargets.Assembly, AllowMultiple=false, Inherited=true)>]
type PackageDescriptionAttribute(t:Type) =
  inherit Attribute()
  let mutable name = "Package name"
  let mutable desc = "Package description"

  /// <summary>
  /// Type of the package manager.
  /// </summary>
  member x.PackageType
    with get() = t
  
  /// <summary>
  /// Name of the package
  /// </summary>
  member x.Name
    with get() = name
    and set(v:string) = name <- v
  
  /// <summary>
  /// Package description
  /// </summary>
  member x.Description
    with get() = desc
    and set(v:string) = desc <- v  

/// <summary>
/// Base class for package manager that should be provided by a VSLab package.
/// Packages are guaranteed to be loaded only once.
/// </summary>
[<AbstractClass>]
type Package() = 
  /// <summary>
  /// This method is invoked when the package is loaded.
  /// </summary>
  abstract member Init : unit -> unit
  default x.Init() = ()
  
  /// <summary>
  /// If the package has been marked for auto open namespaces this method
  /// is invoked.
  /// </summary>
  abstract member OpenNames : unit -> unit
  default x.OpenNames() = ()  
  
  /// <summary>
  /// Return the name of the package.
  /// </summary>
  member x.Name
    with get() =
      let t = x.GetType()
      let attrs = t.GetCustomAttributes(typeof<PackageDescriptionAttribute>, true)
      (attrs.[0] :?> PackageDescriptionAttribute).Name

  /// <summary>
  /// Return the description of the package.
  /// </summary>
  member x.Description
    with get() =
      let t = x.GetType()
      let attrs = t.GetCustomAttributes(typeof<PackageDescriptionAttribute>, true)
      (attrs.[0] :?> PackageDescriptionAttribute).Description

/// <summary>
/// State of a package used by configuration.
/// </summary>
/// <param name="g">True if it is in the global package cache</param>
/// <param name="n">Name of the package</param>
/// <param name="d">Description of the package</param>
/// <param name="l">Location of the package</param>
type PackageState(g:bool, n:string, d:string, l:string) =
  let mutable loadonstartup = false
  let mutable openns = false
  
  /// <summary>
  /// True if the package is global, false if it is in the user package cache.
  /// </summary>
  member x.IsGlobalPackage
    with get() = g
  
  /// <summary>
  /// True if the package should be loaded at startup, false otherwise.
  /// </summary>
  member x.LoadOnStartup
    with get() = loadonstartup
    and set(v:bool) = loadonstartup <- v
  
  /// <summary>
  /// True if the namespaces of the package must be opened automatically, false otherwise.
  /// </summary>
  member x.OpenNS
    with get() = openns
    and set(v:bool) = openns <- v
  
  /// <summary>
  /// Name of the package
  /// </summary>
  member x.PackageName
    with get() = n
  
  /// <summary>
  /// Description of the package
  /// </summary>
  member x.PackageDescription
    with get() = d
  
  /// <summary>
  /// Location of the package
  /// </summary>
  member x.PackageLocation
    with get() = l
  
  /// <summary>
  /// Whether the package is loaded or not.
  /// </summary>
  member x.PackageLoaded
    with get() = if LoadedPackages.ContainsKey(l) && LoadedPackages.[l] then "" else "Load"

/// <summary>
/// Directory containing global installed packages.
/// </summary>
let GlobalPackageDir = VSLabInstallDir() + "packages\\"

/// <summary>
/// Directory containing user installed packages.
/// </summary>
let UserPackageDir = VSLabUserDataDir + "packages\\"

if not(Directory.Exists(UserPackageDir)) then
  Directory.CreateDirectory(UserPackageDir) |> ignore

/// <summary>
/// Check a file if it is a valid VSLab package
/// </summary>
/// <param name="s">The full path to the assembly package</param>
/// <returns>True if the specified file is a valid package, false otherwise</returns>
/// <remarks>Reflection is used and the package gets loaded inside the runtime</remarks>
let CheckPackage (s:string) =
  try
    let a = Assembly.LoadFrom(s)
    let attrs = a.GetCustomAttributes(true) |> Array.filter (fun (o:obj) -> (typeof<PackageDescriptionAttribute>).IsInstanceOfType(o)) |> Array.map (fun (o:obj) -> o :?> PackageDescriptionAttribute)
    attrs.Length = 1
  with
  | _ -> false

/// <summary>
/// Get the information about a package contained in the PackageDescriptionAttribute
/// within the package.
/// </summary>
/// <param name="s">The full path to the assembly package</param>
/// <param name="glob">True if the package is in the local cache false otherwise.</param>
/// <returns>
/// An optional tuple structured as follows
/// (location:string, global:bool, attribute:PackageDescriptionAttribute) option
/// </returns>
/// <remarks>Reflection is used and the package gets loaded inside the runtime</remarks>
let internal GetPackageInfo (s:string) (glob:bool) =
  try
    let a = Assembly.LoadFrom(s)
    let attrs = a.GetCustomAttributes(true) |> Array.filter (fun (o:obj) -> (typeof<PackageDescriptionAttribute>).IsInstanceOfType(o)) |> Array.map (fun (o:obj) -> o :?> PackageDescriptionAttribute)
    if attrs.Length <> 1 then
      Option.None
    else
      Option.Some(s, glob, attrs.[0])
  with
  | _ -> Option.None

/// <summary>
/// Retrieves the PackageDescriptionAttribute inside an assembly.
/// </summary>
/// <param name="a">Assembly to be inspected</param>
/// <returns>
/// The package description attribute.
/// (location:string, global:bool, attribute:PackageDescriptionAttribute) option
/// </returns>
/// <remarks>Reflection is used and the package gets loaded inside the runtime</remarks>
let internal GetPackageDescription (a:Assembly) = 
  let attrs = a.GetCustomAttributes(true) |> Array.filter (fun (o:obj) -> (typeof<PackageDescriptionAttribute>).IsInstanceOfType(o)) |> Array.map (fun (o:obj) -> o :?> PackageDescriptionAttribute)
  attrs.[0]

/// <summary>
/// Sequence of packages available in the cache.
/// </summary>
/// <returns>A sequence of package info tuple for all the packages installed.</returns>
let internal GetPackages () =
  seq { for f in Directory.GetFiles(UserPackageDir) do
          yield GetPackageInfo f false
        for f in Directory.GetFiles(GlobalPackageDir) do
          yield GetPackageInfo f true }

/// <summary>
/// Read the package configuration from the configuration file stored in
/// VSLabUserDataDir + "\packages.xml" combining the information with the list
/// of installed packages.
/// </summary>
/// <returns>The array of PackageState objects describing the installed packages</returns>
let internal ReadPackageConfiguration () =
  let SomeSeq s =
    Seq.filter (fun (v:'a option) -> Option.is_some v) s |> Seq.map (fun (v:'a option) -> Option.get(v))
  let p = Seq.map (fun ((loc:string), (gl:bool), (attr:PackageDescriptionAttribute)) -> new PackageState(gl, attr.Name, attr.Description, loc)) (SomeSeq(GetPackages()))
  let pkgs = new System.Collections.Generic.Dictionary<string, PackageState>()
  for v in p do
    pkgs.Add(v.PackageLocation, v)
    
  if File.Exists(VSLabUserDataDir + "packages.xml") then
    let xml = new XmlDocument()
    xml.Load(VSLabUserDataDir + "packages.xml")
    for v in xml.SelectNodes("/packages/package") do
      let n = (v.Attributes.GetNamedItem("Name") :?> XmlAttribute).Value
      let st = (v.Attributes.GetNamedItem("LoadOnStartup") :?> XmlAttribute).Value = "true"
      let on = (v.Attributes.GetNamedItem("OpenNamespace") :?> XmlAttribute).Value = "true"
      if pkgs.ContainsKey(n) then
        let el = pkgs.[n]
        el.LoadOnStartup <- st
        el.OpenNS <- on
  
  Array.of_seq pkgs.Values
  
/// <summary>
/// Writes the package configuration into the configuration file stored in
/// VSLabUserDataDir + "\packages.xml".
/// </summary>
/// <param name="a">The array of package state objects to be written.</param>
/// <returns>The array of PackageState objects describing the installed packages</returns>
let internal WritePackageConfiguration (a:PackageState array) =
  if File.Exists(VSLabUserDataDir + "packages.xml") then
    File.Delete(VSLabUserDataDir + "packages.xml")
  let xml = new XmlDocument()
  let root = xml.CreateElement("packages")
  for v in a do
    if v.LoadOnStartup || v.OpenNS then
      let n = xml.CreateElement("package")
      let na = xml.CreateAttribute("Name")
      let sa = xml.CreateAttribute("LoadOnStartup")
      let oa = xml.CreateAttribute("OpenNamespace")
      na.Value <- v.PackageLocation
      sa.Value <- if v.LoadOnStartup then "true" else "false"
      oa.Value <- if v.OpenNS then "true" else "false"
      n.Attributes.Append(na) |> ignore
      n.Attributes.Append(sa) |> ignore
      n.Attributes.Append(oa) |> ignore
      root.AppendChild(n) |> ignore
  
  xml.AppendChild(root) |> ignore
  let w = XmlWriter.Create(VSLabUserDataDir + "packages.xml")
  xml.WriteTo(w)
  w.Close()

/// <summary>
/// Checks wether a pacakge has been marked as auto-open namespaces.
/// </summary>
/// <param name="s">Path to the package</param>
/// <returns>True if the package is marked for open namespaces at load time.</returns>
let internal CheckOpenNS(s:string) =
  if File.Exists(VSLabUserDataDir + "packages.xml") then
    let xml = new XmlDocument()
    xml.Load(VSLabUserDataDir + "packages.xml")
    xml.SelectNodes("/packages/package[@Name=\""+s+"\" and @OpenNamespace=\"true\"]").Count = 1
  else
    false

/// <summary>
/// Load a package given the filename.
/// </summary>
/// <param name="pkg">Path to the DLL.</param>
let Load (pkg:string) =
  if not(CheckPackage pkg) then
    failwith ("Unable to locate or load package " + pkg)
  let f = new FileInfo(pkg)
  if not(LoadedPackages.ContainsKey(f.FullName)) then
    VSLab.Environment.RequireDll(f.FullName)
    let assembly = Assembly.LoadFile(f.FullName)
    let desc = GetPackageDescription assembly
    let t = desc.PackageType
    let o = Activator.CreateInstance(t) :?> Package
    o.Init()
    if CheckOpenNS(f.FullName) then
      o.OpenNames()
  
    LoadedPackages.Add(f.FullName, true)    

/// <summary>
/// Load all the packages marked for load at startup
/// </summary>
let LoadStartupPackages() =
  if File.Exists(VSLabUserDataDir + "packages.xml") then
    let xml = new XmlDocument()
    xml.Load(VSLabUserDataDir + "packages.xml")
    for v in xml.SelectNodes("/packages/package[@LoadOnStartup=\"true\"]") do
      Load((v.Attributes.GetNamedItem("Name") :?> XmlAttribute).Value)

/// <summary>
/// Load a package given the name looking in the package folders. The user folder is
/// searched first.
/// </summary>
/// <param name="n">The name of the DLL of the package without the '.dll' extension.</param>
let LoadPackage (n:string) =
  if File.Exists(UserPackageDir + n + ".dll") then
    Load(UserPackageDir + n + ".dll")
  elif File.Exists(GlobalPackageDir + n + ".dll") then
    Load(GlobalPackageDir + n + ".dll")
  else
    failwith ("Unable to load package " + n)

/// <summary>
/// Install or remove a package in the user cache.
/// </summary>
/// <param name="s">Path to the package to install, or the name of the .dll if to be removed</param>
/// <param name="remove">True if the package is to be removed.</param>
let InstallUserPackage (s:string) (remove:bool) =
  try
    if CheckPackage(s) then
      let fn = UserPackageDir + (new FileInfo(s)).Name
      if remove then
        if not(File.Exists(fn)) then
          false
        else
          File.Delete(fn)
          true
      else
        File.Copy(s, fn, true)
        true
    else
      false
  with
  |_ -> false

/// <summary>
/// Install or remove a package in the global cache.
/// </summary>
/// <param name="s">Path to the package to install, or the name of the .dll if to be removed</param>
/// <param name="remove">True if the package is to be removed.</param>
/// <remarks>On Windows Vista with active UAC the user is prompted for elevation</remarks>
let InstallGlobalPackage (s:string) (remove:bool) =
  if CheckPackage(s) then
    let p = VSRunElevate (VSLabInstallDir() + "install-package.exe") ((if remove then "-remove " else "") + "-global \"" + s + "\"")
    if p <> null then
      p.WaitForExit()
      p.ExitCode = 0
    else
      false
  else
    false
